2
0

page-data-props.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import type { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
  2. import type {
  3. IDataWithRequiredMeta,
  4. IPage,
  5. IPageInfoBasic,
  6. IPageNotFoundInfo,
  7. IUser,
  8. } from '@growi/core';
  9. import { isIPageInfo, isIPageNotFoundInfo } from '@growi/core';
  10. import {
  11. isPermalink as _isPermalink,
  12. isTopPage,
  13. isUserPage,
  14. isUsersTopPage,
  15. } from '@growi/core/dist/utils/page-path-utils';
  16. import { removeHeadingSlash } from '@growi/core/dist/utils/path-utils';
  17. import assert from 'assert';
  18. import type { HydratedDocument, model } from 'mongoose';
  19. import type { CrowiRequest } from '~/interfaces/crowi-request';
  20. import type { PageDocument, PageModel } from '~/server/models/page';
  21. import type {
  22. IPageRedirect,
  23. PageRedirectModel,
  24. } from '~/server/models/page-redirect';
  25. import { findPageAndMetaDataByViewer } from '~/server/service/page/find-page-and-meta-data-by-viewer';
  26. import type { CommonEachProps } from '../common-props';
  27. import type {
  28. GeneralPageInitialProps,
  29. IPageToShowRevisionWithMeta,
  30. } from '../general-page';
  31. import type { EachProps } from './types';
  32. // Utility to resolve path, redirect, and identical path page check
  33. type PathResolutionResult = {
  34. resolvedPagePath: string;
  35. isIdenticalPathPage: boolean;
  36. redirectFrom?: string;
  37. };
  38. let mongooseModel: typeof model;
  39. let Page: PageModel;
  40. let PageRedirect: PageRedirectModel;
  41. async function initModels(): Promise<void> {
  42. if (mongooseModel == null) {
  43. mongooseModel = (await import('mongoose')).model;
  44. }
  45. if (Page == null) {
  46. Page = mongooseModel<IPage, PageModel>('Page');
  47. }
  48. if (PageRedirect == null) {
  49. PageRedirect = mongooseModel<IPageRedirect, PageRedirectModel>(
  50. 'PageRedirect',
  51. );
  52. }
  53. }
  54. async function resolvePathAndCheckIdentical(
  55. path: string,
  56. user: IUser | undefined,
  57. ): Promise<PathResolutionResult> {
  58. await initModels();
  59. const isPermalink = _isPermalink(path);
  60. let resolvedPagePath = path;
  61. let redirectFrom: string | undefined;
  62. let isIdenticalPathPage = false;
  63. if (!isPermalink) {
  64. const chains = await PageRedirect.retrievePageRedirectEndpoints(path);
  65. if (chains != null) {
  66. resolvedPagePath = chains.end.toPath;
  67. redirectFrom = chains.start.fromPath;
  68. }
  69. const multiplePagesCount = await Page.countByPathAndViewer(
  70. resolvedPagePath,
  71. user,
  72. null,
  73. true,
  74. );
  75. isIdenticalPathPage = multiplePagesCount > 1;
  76. }
  77. return { resolvedPagePath, isIdenticalPathPage, redirectFrom };
  78. }
  79. /**
  80. * Convert pathname based on page data and permalink status
  81. * @returns Final pathname to be used in the URL
  82. */
  83. function resolveFinalizedPathname(
  84. pagePath: string,
  85. page: HydratedDocument<IPage> | null | undefined,
  86. isPermalink: boolean,
  87. ): string {
  88. let finalPathname = pagePath;
  89. if (page != null) {
  90. // /62a88db47fed8b2d94f30000 ==> /path/to/page
  91. if (isPermalink && page.isEmpty) {
  92. finalPathname = page.path;
  93. }
  94. // /path/to/page ==> /62a88db47fed8b2d94f30000
  95. if (!isPermalink && !page.isEmpty) {
  96. const isToppage = isTopPage(pagePath);
  97. if (!isToppage && page._id) {
  98. finalPathname = `/${page._id.toString()}`;
  99. }
  100. }
  101. }
  102. return finalPathname;
  103. }
  104. // Page data retrieval for initial load - returns GetServerSidePropsResult
  105. export async function getPageDataForInitial(
  106. context: GetServerSidePropsContext,
  107. ): Promise<
  108. GetServerSidePropsResult<
  109. Pick<GeneralPageInitialProps, 'pageWithMeta' | 'skipSSR'> &
  110. Pick<
  111. EachProps,
  112. 'currentPathname' | 'isIdenticalPathPage' | 'redirectFrom'
  113. >
  114. >
  115. > {
  116. const req: CrowiRequest = context.req as CrowiRequest;
  117. const { crowi, user } = req;
  118. const { revisionId } = req.query;
  119. // Parse path from URL
  120. let { path: pathFromQuery } = context.query;
  121. pathFromQuery = pathFromQuery != null ? (pathFromQuery as string[]) : [];
  122. let pathFromUrl = `/${pathFromQuery.join('/')}`;
  123. pathFromUrl = pathFromUrl === '//' ? '/' : pathFromUrl;
  124. const { pageService, pageGrantService, configManager } = crowi;
  125. const pageId = _isPermalink(pathFromUrl)
  126. ? removeHeadingSlash(pathFromUrl)
  127. : null;
  128. const isPermalink = _isPermalink(pathFromUrl);
  129. const { resolvedPagePath, isIdenticalPathPage, redirectFrom } =
  130. await resolvePathAndCheckIdentical(pathFromUrl, user);
  131. if (isIdenticalPathPage) {
  132. return {
  133. props: {
  134. currentPathname: resolvedPagePath,
  135. isIdenticalPathPage: true,
  136. pageWithMeta: null,
  137. skipSSR: false,
  138. redirectFrom,
  139. },
  140. };
  141. }
  142. // Get full page data
  143. const pageWithMeta = await findPageAndMetaDataByViewer(
  144. pageService,
  145. pageGrantService,
  146. { pageId, path: resolvedPagePath, user },
  147. );
  148. const isHidingUserPages = configManager.getConfig(
  149. 'security:isHidingUserPages',
  150. );
  151. if (isHidingUserPages && pageWithMeta.data != null) {
  152. const pagePath = pageWithMeta.data.path;
  153. const isTargetUserPage = isUserPage(pagePath) || isUsersTopPage(pagePath);
  154. if (isTargetUserPage) {
  155. const isOwnPage =
  156. user != null &&
  157. (pagePath === `/user/${user.username}` ||
  158. pagePath.startsWith(`/user/${user.username}/`));
  159. if (!isOwnPage) {
  160. return {
  161. props: {
  162. currentPathname: resolvedPagePath,
  163. isIdenticalPathPage: false,
  164. pageWithMeta: {
  165. data: null,
  166. meta: {
  167. isNotFound: true,
  168. isForbidden: true,
  169. },
  170. } satisfies IDataWithRequiredMeta<null, IPageNotFoundInfo>,
  171. skipSSR: false,
  172. redirectFrom,
  173. },
  174. };
  175. }
  176. }
  177. }
  178. // Handle URL conversion
  179. const currentPathname = resolveFinalizedPathname(
  180. resolvedPagePath,
  181. pageWithMeta.data,
  182. isPermalink,
  183. );
  184. // When the page exists
  185. if (pageWithMeta.data != null) {
  186. const { data: page, meta } = pageWithMeta;
  187. // type assertion
  188. assert(isIPageInfo(meta), 'meta should be IPageInfo when data is not null');
  189. // Handle empty pages - return as not found to avoid serialization issues
  190. if (page.isEmpty) {
  191. return {
  192. props: {
  193. currentPathname,
  194. isIdenticalPathPage: false,
  195. pageWithMeta: {
  196. data: null,
  197. meta: {
  198. isNotFound: true,
  199. isForbidden: false,
  200. },
  201. } satisfies IDataWithRequiredMeta<null, IPageNotFoundInfo>,
  202. skipSSR: false,
  203. redirectFrom,
  204. },
  205. };
  206. }
  207. // Handle existing page with valid meta that is not IPageNotFoundInfo
  208. page.initLatestRevisionField(revisionId);
  209. const ssrMaxRevisionBodyLength = configManager.getConfig(
  210. 'app:ssrMaxRevisionBodyLength',
  211. );
  212. // Check if SSR should be skipped
  213. const latestRevisionBodyLength = await page.getLatestRevisionBodyLength();
  214. const skipSSR =
  215. latestRevisionBodyLength != null &&
  216. ssrMaxRevisionBodyLength < latestRevisionBodyLength;
  217. const populatedPage = await page.populateDataToShowRevision(skipSSR);
  218. return {
  219. props: {
  220. currentPathname,
  221. isIdenticalPathPage: false,
  222. pageWithMeta: {
  223. data: populatedPage,
  224. meta,
  225. } satisfies IPageToShowRevisionWithMeta,
  226. skipSSR,
  227. redirectFrom,
  228. },
  229. };
  230. }
  231. // type assertion
  232. assert(
  233. isIPageNotFoundInfo(pageWithMeta.meta),
  234. 'meta should be IPageNotFoundInfo when data is null',
  235. );
  236. // Handle the case where the page does not exist
  237. return {
  238. props: {
  239. currentPathname: resolvedPagePath,
  240. isIdenticalPathPage: false,
  241. pageWithMeta: pageWithMeta satisfies IDataWithRequiredMeta<
  242. null,
  243. IPageNotFoundInfo
  244. >,
  245. skipSSR: false,
  246. redirectFrom,
  247. },
  248. };
  249. }
  250. // Page data retrieval for same-route navigation
  251. export async function getPageDataForSameRoute(
  252. context: GetServerSidePropsContext,
  253. ): Promise<{
  254. props: Pick<CommonEachProps, 'currentPathname'> &
  255. Pick<EachProps, 'currentPathname' | 'isIdenticalPathPage' | 'redirectFrom'>;
  256. internalProps?: {
  257. pageWithMeta?:
  258. | IDataWithRequiredMeta<PageDocument, IPageInfoBasic>
  259. | IDataWithRequiredMeta<null, IPageNotFoundInfo>;
  260. };
  261. }> {
  262. const req: CrowiRequest = context.req as CrowiRequest;
  263. const { crowi, user } = req;
  264. const { pageService, pageGrantService } = crowi;
  265. const pathname = decodeURIComponent(
  266. context.resolvedUrl?.split('?')[0] ?? '/',
  267. );
  268. const pageId = _isPermalink(pathname) ? removeHeadingSlash(pathname) : null;
  269. const isPermalink = _isPermalink(pathname);
  270. const { resolvedPagePath, isIdenticalPathPage, redirectFrom } =
  271. await resolvePathAndCheckIdentical(pathname, user);
  272. if (isIdenticalPathPage) {
  273. return {
  274. props: {
  275. currentPathname: resolvedPagePath,
  276. isIdenticalPathPage: true,
  277. redirectFrom,
  278. },
  279. };
  280. }
  281. // For same route access, do minimal page lookup
  282. const pageWithMetaBasicOnly = await findPageAndMetaDataByViewer(
  283. pageService,
  284. pageGrantService,
  285. { pageId, path: resolvedPagePath, user, basicOnly: true },
  286. );
  287. const currentPathname = resolveFinalizedPathname(
  288. resolvedPagePath,
  289. pageWithMetaBasicOnly.data,
  290. isPermalink,
  291. );
  292. return {
  293. props: {
  294. currentPathname,
  295. isIdenticalPathPage: false,
  296. redirectFrom,
  297. },
  298. internalProps: {
  299. pageWithMeta: pageWithMetaBasicOnly.data?.isEmpty
  300. ? {
  301. data: null,
  302. meta: { isNotFound: true, isForbidden: false },
  303. }
  304. : pageWithMetaBasicOnly,
  305. },
  306. };
  307. }